﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

using Effects.Effects;

namespace Effects.GUI
{

    /// <summary>
    ///  Manages settings changes of one effect
    /// </summary>
    [Serializable()]
    public class ChangesManager : ISerializable
    {
        #region mode
        // the modes the manager can work in

        /// <summary>
        ///  Enumeration of different states EffectSettingsManager can be.
        /// </summary>
        /// <remarks>
        /// <para> Off - settings manager is off. It doesn't record or play changes. </para>
        /// <para> Playing - settings manager plays the changes </para>
        /// <para> Recording - settings manager records the changes </para>
        ///  Off - 
        /// </remarks>
        public enum ManagingMode 
        { 
            /// <summary>
            ///  Settings manager is off. It doesn't record or play changes.
            /// </summary>
            Off, 
            
            /// <summary>
            ///  Playing - settings manager plays the changes
            /// </summary>
            Playing, 
            
            /// <summary>
            ///  Recording - settings manager records the changes
            /// </summary>
            Recording 
        }

        /// <summary>
        ///  information about current mode of the settings manager
        /// </summary>
        [NonSerialized()]
        private ManagingMode currentMode;

        /// <summary>
        ///  Gets or sets the mode of the settings manager.
        /// </summary>
        public ManagingMode Mode
        {
            get { return currentMode; }
            set 
            {
                lock(locker)
                {
                    ManagingMode old = currentMode;
                    currentMode = value;

                    // switch from recording to playing
                    if (currentMode == ManagingMode.Playing)
                    {
                        if (old != ManagingMode.Playing)
                        {
                            changesPlayCopy = Clone(changes);
                            pointer = FindFirstAfter(lastPlayerTime, changesPlayCopy, changesPlayCopy.First);
                            return;
                        }
                    }
                        
                    // switch from playing to recording
                    if (old == ManagingMode.Playing && currentMode == ManagingMode.Recording)
                    {
                        pointer = FindFirstAfter(lastPlayerTime, changes, changes.First);
                        return;
                    }
                }
            }
        }

        /// <summary>
        ///  Sets Mode to Off.
        /// </summary>
        public void SetModeOff()
        {
            Mode = ManagingMode.Off;
        }

        /// <summary>
        ///  Sets Mode to  Playing
        /// </summary>
        public void SetModePlaying()
        {
            Mode = ManagingMode.Playing;

        }
        
        /// <summary>
        ///  Sets Mode to Recording
        /// </summary>
        public void SetModeRecording()
        {
            Mode = ManagingMode.Recording;
        }
        #endregion

        #region managed_effect
        // effect that is managed by the class

        /// <summary>
        ///  Effect to be managed.
        /// </summary>
        [NonSerialized()]
        private Effect effect;

        /// <summary>
        ///  Stores name of the managed effect.
        /// </summary>
        /// <remarks>
        ///  This memeber is used only when manager is desrializes and is given effect to manage. This effect mus have same type as manager
        ///  has managed previously.
        /// </remarks>
        private string effectName;

        /// <summary>
        ///  Gets or sets the managed effect.
        /// </summary>
        /// <remarks>
        ///  <para>
        ///   When new effect is set, it must be of the same type as the one the manager had been used previously.
        ///  </para>
        /// </remarks>
        public Effect Effect
        {
            get { return effect; }
            set
            {
                if (this.effectName == value.Name)
                {
                    if (this.effect != null)
                        this.effect.SettingsChanged -= this.NewChange;

                        this.effect = value;
                        this.effect.SettingsChanged += this.NewChange;
                        this.effectName = this.effect.Name;
                    }
            }
        }

        #endregion

        #region effect_library
        // current effect library of the application

        /// <summary>
        ///  Effect Library to create new instances of effects 
        /// </summary>
        [NonSerialized()]
        private EffectLibrary library;

        /// <summary>
        ///  Gets or sets the EffectLibrary used in manager.
        /// </summary>
        public EffectLibrary Library
        {
            get { return library; }
            set { library = value; }
        }

        #endregion

        #region lists_of_changes

        /// <summary>
        ///  One node in the linked list of changes.
        /// </summary>
        [Serializable()]
        public class Node
        {
            /// <summary>
            ///  The actual change.
            /// </summary>
            public EffectSettingsChangedEventArgs change;

            /// <summary>
            ///  The time when change occured.
            /// </summary>
            public int time;

            /// <summary>
            ///  Creates new instance of Node.
            /// </summary>
            /// <param name="change"> Change arguments specifies the change. </param>
            /// <param name="time"> Time when the change occurred. </param>
            public Node(EffectSettingsChangedEventArgs change, int time)
            {
                this.change = change;
                this.time = time;
            }
        }

        /// <summary>
        ///  List of changes that are recorded;
        /// </summary>
        private LinkedList<Node> changes;

        /// <summary>
        ///  Gets or sets list of recorded changes.
        /// </summary>
        public LinkedList<Node> Changes
        {
            get { return changes; }
            set
            {
                this.changes = value;
                pointer = changes.First;
                
            }
        }

        /// <summary>
        ///  Copy that is used when playing (NO RECORDING)
        /// </summary>
        /// <remarks>
        ///  <para>
        ///  This linked list is temporary. It is used when playing the cahnges and ClearLaterChanges is turned on. In that case if any new change
        ///  occures all later changes are deleted, but it don't affect the recorded changes. This change is only valid until Stop is pressed or end of file occures.
        ///  </para>
        /// </remarks>
        private LinkedList<Node> changesPlayCopy;

        /// <summary>
        ///  Pointer that tells current position to one of changes list.
        /// </summary>
        private LinkedListNode<Node> pointer;

        #endregion

        #region initial_settings
        // settings of the effect at the beginning of the file.

        /// <summary>
        ///  The initial settings of the effect.
        /// </summary>
        /// <remarks>
        ///  By the term "initial settings" we mean the settings that the effect has at the begin of the file.
        /// </remarks>
        [NonSerialized()]
        private object[] initialSettings;

        /// <summary>
        ///  Gets or sets initial settings of the effect.
        /// </summary>
        /// <remarks>
        ///  By the term "initial settings" we mean the settings that the effect has at the begin of the file.
        /// </remarks>
        public object[] Initials
        {
            get { return initialSettings; }
            set { initialSettings = value; }
        }

        #endregion

        #region clear_later_changes
        // turning on/off clear later changes mode.

        /// <summary>
        ///  clearing changes that are later in the list when adding new one.
        /// </summary>
        private bool clearLaterChanges;

        /// <summary>
        ///  Gets or sets the ClearLaterChanges property.
        /// </summary>
        /// <remarks>
        ///  <para>
        ///   This property specifies behaviour of manager when new settings change is added. When true all alter changes are deleted.
        ///  </para>
        /// </remarks>
        public bool ClearLaterChanges
        {
            get { return clearLaterChanges; }
            set { clearLaterChanges = value; }
        }

        /// <summary>
        ///  Turns on/off Clear Later changes mode.
        /// </summary>
        /// <param name="on"> true - on, false - off </param>
        public void SetClearLaterChanges(bool on)
        {
            this.ClearLaterChanges = on;
        }

        #endregion

        #region player
        // player to refer for current time.

        /// <summary>
        ///  Player that uses the effect.
        /// </summary>
        /// <remarks>
        ///  This reference is used to determine the current time whenever it is needed.
        /// </remarks>
        [NonSerialized()]
        private AudioPlayer player;

        /// <summary>
        ///  Gets or sets the player
        /// </summary>
        public AudioPlayer Player
        {
            get { return player; }
            set
            {
                if (player != null)
                    player.EndOfFile -= this.PlayerEndOfFile;
                player = value;
                if(player != null)
                    player.EndOfFile += this.PlayerEndOfFile;
            }
        }

        /// <summary>
        ///  Sets used AudioPlayer.
        /// </summary>
        /// <param name="player"> AudioPlayer to get time from. </param>
        public void SetPlayer(AudioPlayer player)
        {
            if (this.player != null)
                this.player.EndOfFile -= this.PlayerEndOfFile;
            this.player = player;
            if (this.player != null)
                this.player.EndOfFile += this.PlayerEndOfFile;
        }

        #endregion

        #region timer

        /// <summary>
        ///  Timer that forces manager to apply changes.
        /// </summary>
        /// <remarks>
        ///  <para>
        ///   Timer is necessary to make the whole thing working. It periodically checks the time od player and apply cahnges that happen since last check.
        ///  </para>
        /// </remarks>
        private System.Timers.Timer timer;

        /// <summary>
        ///  Time when timer has ticked last time.
        /// </summary>
        private int lastPlayerTime;

        #endregion

        #region locker

        /// <summary>
        ///  Multithread access lock.
        /// </summary>
        private object locker;

        #endregion

        #region constructor

        /// <summary>
        ///  Creates new intance that will manage given effect.
        /// </summary>
        /// <param name="e"> Effect to manage </param>
        public ChangesManager(Effect e)
        {
            this.effect = e;
            this.effectName = e.Name;
            this.effect.SettingsChanged += this.NewChange;
            this.changes = new LinkedList<Node>();
            this.changesPlayCopy = new LinkedList<Node>();

            this.locker = new object();
            this.recordedSomething = true;

            this.timer = new System.Timers.Timer(100);
            this.timer.Elapsed += new System.Timers.ElapsedEventHandler(this.TimerTick);
        }

        #endregion

        #region initial_settings
        // work with initial settings

        /// <summary>
        ///  Saves current effect settings as initial.
        /// </summary>
        public void GetInitialSettings()
        {
            this.initialSettings = this.effect.GetAllSettings();
        }

        /// <summary>
        ///  Sets stored initial settings to the effect.
        /// </summary>
        public void SetInitialSettings()
        {
            if(this.initialSettings != null)
                this.effect.SetAllSettings(initialSettings);
        }
        #endregion
        
        #region commands
        // Play Pause Stop Record RecordNew

        /// <summary>
        ///  Forces ChangeManager to controll playing.
        /// </summary>
        public void Play()
        {
            lock (locker)
            {
                if (currentMode == ManagingMode.Off)
                    return;

                // is stopped file is at begin
                if (stopped)
                {
                    stopped = false;
                    // create play copy
                    changesPlayCopy = Clone(changes);
                    // set pointer to the begining of play copy
                    pointer = changesPlayCopy.First;

                }

                // run timer
                timer.Start();
            }
        }

        /// <summary>
        ///  Pauses ChangesManager
        /// </summary>
        public void Pause()
        {
            lock (locker)
            {
                if (currentMode == ManagingMode.Off)
                    return;

                timer.Stop();
            }
        }

        /// <summary>
        ///  Indicates wheter manager is currently stopped;
        /// </summary>
        /// <remarks>
        ///  Used when strating to play/record to determine whether the manager is stopped or paused.
        /// </remarks>
        private bool stopped;
        
        /// <summary>
        ///  Stops ChangesManager and resets effect settings to initials.
        /// </summary>
        public void Stop()
        {
            lock (locker)
            {
                // when off there is nothing to do
                if (currentMode == ManagingMode.Off)
                    return;

                stopped = true;
                // stop the timer
                timer.Stop();
                lastPlayerTime = 0;

                // set the initial settings
                Reset();
            }
        }

        /// <summary>
        ///  Stops ChangesManager but doesn't resests effect's settings to initials.
        /// </summary>
        public void StopNoReset()
        {
            lock (locker)
            {
                // when off there is nothing to be done
                if (currentMode == ManagingMode.Off)
                    return;

                // stop the timer
                timer.Stop();

                stopped = true;
                lastPlayerTime = 0;

                pointer = changes.First;
            }
        }

        /// <summary>
        ///  Indicates whether there have been any changes recorded.
        /// </summary>
        private bool recordedSomething;

        /// <summary>
        ///  Starts recording new changes.
        /// </summary>
        public void Record()
        {
            lock (locker)
            {
                this.currentMode = ManagingMode.Recording;
                // still havent recorded anything so pretend to Record new
                if (!recordedSomething)
                {
                    RecordNew();
                    return;
                }

                if (stopped)
                {
                    stopped = false;
                    pointer = changes.First;
                }
                timer.Start();
            }
        }

        /// <summary>
        ///  Clear everything recorded previously. Saves current effect's settings as initial settings.
        /// </summary>
        public void RecordNew()
        {
            lock (locker)
            {
                if (currentMode == ManagingMode.Off)
                    return;

                this.currentMode = ManagingMode.Recording;

                StopNoReset();
                GetInitialSettings();
                changes = new LinkedList<Node>();
                pointer = changes.First;
                recordedSomething = true;

                timer.Start();
            }
        }

        /// <summary>
        ///  Resets <see cref="ChangesManager"/>
        /// </summary>
        /// <remarks>
        ///  <para>
        ///   Forces <see cref="ChangesManager"/> to move back the beginning of recorded changes and to set initial settings to the managed <see cref="Effect"/>
        ///  </para>
        /// </remarks>
        public void Reset()
        {
            pointer = changes.First;
            SetInitialSettings();
        }

        /// <summary>
        ///  Player EndOfFile event handler. Forces <see cref="ChangesManager"/> to stop.
        /// </summary>
        /// <param name="sender"> Object that raised the event </param>
        /// <param name="e"> Event arguments </param>
        private void PlayerEndOfFile(object sender, EventArgs e)
        {
            Stop();
        }

        #endregion

        #region add_new_change
        // adding new change

        /// <summary>
        ///  Event handler for effect.SettingsChange  event.
        /// </summary>
        /// <param name="sender"> Effect that raised the event. </param>
        /// <param name="args"> Event arguments </param>
        /// <remarks>
        ///  <para>
        ///   This method deals with settings changes coming from the managed effect.
        ///  </para>
        ///  <para>
        ///   Depending on current Mode it saves the change (and/or clear the later ones) or just do nothing
        ///  </para>
        /// </remarks>
        private void NewChange(object sender, EffectSettingsChangedEventArgs args)
        {
            if(currentMode == ManagingMode.Off)
                return;

            lock (locker)
            {
                if (player != null)
                {
                    int time = player.PlayPosition;

                    if (currentMode == ManagingMode.Recording)
                    {
                        AddChange(changes,ref pointer, args, time);
                    }
                    else
                    {
                        if (clearLaterChanges)
                            Clear(ref changesPlayCopy, ref pointer);

                    }
                }
            }
        }

        /// <summary>
        ///  Adds change to the list
        /// </summary>
        /// <param name="changes"> List to add the change to </param>
        /// <param name="pointer"> Pointer where add the change </param>
        /// <param name="e"> Argumetns of the change </param>
        /// <param name="time"> Time when change occured </param>
        private void AddChange(LinkedList<Node> changes,ref LinkedListNode<Node> pointer, EffectSettingsChangedEventArgs e, int time)
        {
            lock (locker)
            {
                // pointer is at the end of the list, and therefore we clear nothing
                if (pointer == null)
                {
                    changes.AddLast(new Node(e, time));
                    return;
                }

                // pointer points somewhere in the middle of list
                changes.AddBefore(pointer, new Node(e, time));


                // test the clearing - 
                if (clearLaterChanges)
                    Clear(ref changes, ref pointer);
            }

        }

        #endregion

        #region applying_the changes
        // methods used when playing or processing wave files.

        /// <summary>
        ///  Apply the changes valid until given time.
        /// </summary>
        /// <param name="time"> Time </param>
        /// <remarks>
        ///  <para>
        ///   This method is to be used when proccessing data and writing them to the file. It's called with current time and applies changes that must be done in between
        ///   time given in last call and time give currently.
        ///  </para>
        /// </remarks>
        public void ApplyChanges(int time)
        {
            TimerTickWork(changes,ref pointer, time);
        }

        /// <summary>
        ///  Timer tick event method. Periodicly controls and applies changes to the effect's settings.
        /// </summary>
        /// <param name="sender"> object that has raised the event </param>
        /// <param name="args"> Event arguments </param>
        private void TimerTick(object sender, System.Timers.ElapsedEventArgs args)
        {
            // if manager is off, just doing nothing
            if (currentMode == ManagingMode.Off)
                return;

            lock (locker)
            {
                try
                {
                    // check the lists and look for changes
                    int time = player.PlayPosition;

                    if (time < lastPlayerTime && !stopped)
                    {
                        // player moved back
                        lastPlayerTime = time;

                        // move back + recreate state at the moment
                        if (currentMode == ManagingMode.Recording)
                            GetBackInTime(changes, ref pointer, time, effectName, initialSettings);
                        else
                            GetBackInTime(changesPlayCopy, ref pointer, time, effectName, initialSettings);
                    }

                    // assing the time
                    lastPlayerTime = time;


                    // do the work
                    if (currentMode == ManagingMode.Recording)
                        TimerTickWork(changes, ref pointer, time);
                    else
                        TimerTickWork(changesPlayCopy, ref pointer, time);
                }
                catch (NullReferenceException) // in case when manager is being deleted but timer tick occures
                {  }
            }

        }

        /// <summary>
        ///  Applying the changes.
        /// </summary>
        /// <param name="changes"> List of changes to work with. </param>
        /// <param name="pointer"> Current position in the list. </param>
        /// <param name="time"> Current time = until when apply the changes. </param>
        private void TimerTickWork(LinkedList<Node> changes,ref LinkedListNode<Node> pointer,int time)
        {
            lock (locker)
            {
                while (pointer != null && pointer.Value.time <= time)
                {
                    //board.SetEffectSettings(pointer.Value.change.Effect, (EffectSettingsChangedEventArgs)pointer.Value.change.ChangeData);
                    effect.SetSetting(pointer.Value.change);
                    pointer = pointer.Next;
                }
            }
        }

        /// <summary>
        ///  Method gets back in time.
        /// </summary>
        /// <param name="changes"> list of changes to use </param>
        /// <param name="pointer"> Pointer to the list to move to the right place </param>
        /// <param name="time"> Time to get to </param>
        /// <param name="effect"> Effect we are managing </param>
        /// <param name="initials"> Initial effect settings </param>
        private void GetBackInTime(LinkedList<Node> changes, ref LinkedListNode<Node> pointer,int time, string effect, object[] initials)
        {
            lock (locker)
            {
                if (pointer != null)
                    pointer = FindFirstAfterReverse(time, changes, pointer);
                else
                    pointer = FindFirstAfter(time, changes, changes.First);

                object[] settings = RecreateChanges(changes, pointer,effect ,initials );
                this.effect.SetAllSettings(settings);
            }
        }
        
        /// <summary>
        ///  Recreates effect settings until stop node.
        /// </summary>
        /// <param name="changes"> List of changes to use </param>
        /// <param name="stop"> Stop node </param>
        /// <param name="effect"> Effect's name </param>
        /// <param name="initials"> Effect's initial settings.</param>
        /// <returns> All effect's settings at the moment of stop node. </returns>
        private object[] RecreateChanges(LinkedList<Node> changes, LinkedListNode<Node> stop, string effect, object[] initials)
        {
            Effect e = library.GetEffect(effect);
            e.SetAllSettings(initials);

            LinkedListNode<Node> i = changes.First;
            while (i != stop)
            {
                e.SetSetting(i.Value.change);
                i = i.Next;
            }

            return e.GetAllSettings();
        }

        #endregion

        #region linked_list_work
        //  Few useful routines that work with LinkedList<Node>

        /// <summary>
        ///  Removes all nodes from list that are behind given pointer.
        /// </summary>
        /// <param name="changes"> List to clear </param>
        /// <param name="pointer"> Node to start removing. </param>
        private void Clear(ref LinkedList<Node> changes, ref LinkedListNode<Node> pointer)
        {
            if (pointer != null)
            {
                changes.RemoveFrom(pointer);
                pointer = null;
            }

           /* LinkedListNode<Node> helpPtr;
            while (pointer != null)
            {
                helpPtr = pointer.Next;
                changes.Remove(pointer);
                pointer = helpPtr;
            } */
        }

        /// <summary>
        ///  Creates deep copy of given LinkedList 
        /// </summary>
        /// <param name="list"> LinkedList to copy </param>
        /// <returns> Deep copy of given list </returns>
        private LinkedList<Node> Clone(LinkedList<Node> list)
        {
            System.IO.MemoryStream ms = new System.IO.MemoryStream();
            BinaryFormatter formater = new BinaryFormatter();
            formater.Serialize(ms, list);
            ms.Position = 0;
            return (LinkedList<Node>)formater.Deserialize(ms);
        }

        /// <summary>
        ///  Finds node in the list that is first after given time.
        /// </summary>
        /// <param name="time"> time  </param>
        /// <param name="list"> List to search in </param>
        /// <param name="begin"> Node to start searching from </param>
        /// <returns> First node with time greater than given time or null if it doesn't exist. </returns>
        private LinkedListNode<Node> FindFirstAfter(int time, LinkedList<Node> list, LinkedListNode<Node> begin)
        {
            // if count == 0 then list.First is equal to list.Last equal to null
            if (list.Empty)
                return null; 

            // test where given begin-serch-node belongs to given list
            if (begin.List != list)
                throw new ArgumentException("Given LinkedList list does not contain LinkedListNode begin");

            
            LinkedListNode<Node> i = begin;
            while (i != list.Last)
            {
                if (i.Value.time < time)
                    i = i.Next;
                else
                    return i;
            }
            return i;
        }

        /// <summary>
        ///  Finds first node with time greater than given value of time.
        /// </summary>
        /// <param name="time"> time </param>
        /// <param name="list"> List to search in </param>
        /// <param name="begin"> Node to begin the search </param>
        /// <returns>  </returns>
        /// <remarks>
        ///  Similar method to <see cref="FindFirstAfter"/> but search in opposite direction (It goes from end to the begining od the list)
        /// </remarks>
        private LinkedListNode<Node> FindFirstAfterReverse(int time, LinkedList<Node> list, LinkedListNode<Node> begin)
        {
            if (list.Empty)
                return null;

            if (begin.List != list)
                throw new ArgumentException("Given LinkedList list does not contain LinkedListNode begin");

            LinkedListNode<Node> i = begin;
            while (i != list.First)
            {
                if (i.Value.time >= time)
                    i = i.Previous;
                else
                    return i;
            }
            return i;
        }

        #endregion

        #region serialization

        /// <summary>
        ///  Method used to serialize the ChangesManager
        /// </summary>
        /// <param name="info"> The SerializationInfo to populate with data. </param>
        /// <param name="context"> The destination (see StreamingContext) for this serialization. </param>
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("RecordedSth", recordedSomething);

            // writing the data
            info.AddValue("EffectName", this.effectName);
            info.AddValue("EffectInitials", this.initialSettings);

            // writing the  list of changes
            info.AddValue("EffectChanges", this.changes);
        }

        /// <summary>
        ///  Deserialization constructor
        /// </summary>
        /// <param name="info"> The SerializationInfo to get data from </param>
        /// <param name="context"> The source for this deserialization. </param>
        public ChangesManager(SerializationInfo info, StreamingContext context)
        {
            this.recordedSomething = (bool) info.GetValue("RecordedSth",typeof(bool));

            this.effectName = (string)info.GetValue("EffectName", typeof(string));
            this.initialSettings = (object[]) info.GetValue("EffectInitials", typeof(object[]));

            this.changes = (LinkedList<Node>)info.GetValue("EffectChanges", typeof(LinkedList<Node>));

            this.pointer = changes.First;
            timer = new System.Timers.Timer(100);
            timer.Elapsed += new System.Timers.ElapsedEventHandler(this.TimerTick);
            locker = new object();
            stopped = true;
            Stop();
        }

        #endregion

        #region wave_format_conversion
        // conversion to work with different wave format.

        /// <summary>
        ///  Converts all time values to correspond with new sample rate and number of channels.
        /// </summary>
        /// <param name="oldSampleRate"> Current sampling rate of input. </param>
        /// <param name="oldChannels"> Current number of channels of input. </param>
        /// <param name="newSampleRate"> Desired new sampling rate. </param>
        /// <param name="newChannels"> Desired new number of channels. </param>
        /// <remarks>
        ///  Conversion is needed to keep time values pointing to the point that is same far of the begin of the file.
        ///  For example: let time value be 44100. With sampling rate 44100 and 1 channel, this value points exactly
        ///  at the point 1 second from beginning. But with sampling rate 22050 it would be 2 seconds. And with sampling rate
        ///  44100 and 2 channels it is half a second from beginning.
        /// </remarks>
        public void ConvertTo(int oldSampleRate, int oldChannels, int newSampleRate, int newChannels)
        {
            LinkedListNode<Node> i = changes.First;
            while (i != null)
            {
                i.Value.time = Wave.SampleToSampleConverter.SampleToSample(i.Value.time, oldSampleRate, oldChannels, newSampleRate, newChannels);
                i = i.Next;
            }
        }
        #endregion
    }
}
